/**
 * Copyright 2006 gworks.com.au
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. (http://www.apache.org/licenses/LICENSE-2.0)
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for 
 * the specific language governing permissions and limitations under the License.
 *
 * <dhamma-message src="Atisha (11th century Tibetan Buddhist master)">
 * 	The greatest achievement is selflessness. The greatest worth is self-mastery.
 * 	The greatest quality is seeking to serve others. The greatest precept is continual awareness.
 * 	The greatest medicine is the emptiness of everything. The greatest action is not conforming with the worlds ways.
 * 	The greatest magic is transmuting the passions. The greatest generosity is non-attachment.
 * 	The greatest goodness is a peaceful mind. The greatest patience is humility.
 * 	The greatest effort is not concerned with results. The greatest meditation is a mind that lets go.
 * 	The greatest wisdom is seeing through appearances. 
 * </dhamma-message>
 */
package org.javaongems.server;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.security.Principal;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;
import org.javaongems.runtime.http.ExpressRedirector;
import org.javaongems.runtime.lang.StringUtils;
import org.javaongems.runtime.util.PropertiesUtils;

/**
 * <p>Provides the dynamic entry point to bootstrap a Gems application</p>
 * <p>Responsibilities:
 * <ul>
 * 	<li> support configuration of gwt module name, jsp entry point page, 
 * 		context factories and context resolvers
 * 	<li> inspect each http get/post and determine the appropriate page context 
 * 		factory
 * 	<li> drive the page context factory to build the page and then initialise 
 * 		and render the page 
 * </ul>
 * </p> 
 * <p>Collaborators:
 * <ul>
 * 	<li> {@link ContextFactory} implementations
 * 	<li> {@link ContextFactoryResolver} implementation
 * </ul> 
 * </p>
 * @author Ashin Wimalajeewa (ash)
 */
public class GemEntryPointPager extends HttpServlet {
	final static private Logger LOGGER = Logger.getLogger(GemEntryPointPager.class);
	final static public String CTX_FACTORY_RESOLVER = "context.factory.resolver";
	final static public String CTX_FACTORIES = "context.factories";

	final static private long serialVersionUID = 1L;

	protected ServletContext servletContext;
	protected ServletConfig	servletConfig;
	
	protected String gwtModuleName;
	protected String gwtEntryPointPage;
	protected ContextFactoryResolver factoryResolver;
    protected HashMap<String, ContextFactory> pageFactoryContexts = new HashMap<String, ContextFactory>();
	
	final static private ThreadLocal<HttpServletRequest> threadsRequest = new ThreadLocal<HttpServletRequest>();
	final static private ThreadLocal<Principal> threadsPrincipal = new ThreadLocal<Principal>();
    
	/** 
	 * Called from the servlet container to initialise paging itself before use. 
	 * The following servlet configuration parameters are required:
	 * <ul>
	 * 	<li> gwt:module
	 * 	<li> gwt:entry-point-page
	 *  <li> context.factory.resolver
	 *  <li> context.factories
	 * </ul> 
	 * @see javax.servlet.GenericServlet#init(javax.servlet.ServletConfig)
	 */
	public void init(ServletConfig config) throws ServletException {
		super.init(config);
		servletConfig = config;
		servletContext = servletConfig.getServletContext();
		gwtModuleName = servletConfig.getInitParameter(EntryPointPage.GWT_MODULE_PARAM);
		if (StringUtils.isEmpty(gwtModuleName))
			throw new ServletException("Gem entry point pager needs a value for [" + EntryPointPage.GWT_MODULE_PARAM + "] to be specified in GemEntryPointPager's init-param in web.xml.");
		gwtEntryPointPage = servletConfig.getInitParameter(EntryPointPage.GWT_ENTRY_POINT_PAGE_PARAM);
		if (StringUtils.isEmpty(gwtEntryPointPage))
			throw new ServletException("Gem entry point pager needs a value for [" + EntryPointPage.GWT_ENTRY_POINT_PAGE_PARAM + "] to be specified in GemEntryPointPager's init-param in web.xml.");
		String resolverDetail = servletConfig.getInitParameter(CTX_FACTORY_RESOLVER);
		String contextDetails = servletConfig.getInitParameter(CTX_FACTORIES);
		if (StringUtils.isEmpty(contextDetails))
			throw new ServletException("Gem entry point pager needs a value for [" + CTX_FACTORIES + "] to be specified in GemEntryPointPager's init-param in web.xml.");
		Properties props = PropertiesUtils.toProperties(contextDetails);
		loadContextFactoriesImplementations(props);
		if (StringUtils.isEmpty(resolverDetail))
			throw new ServletException("Gem entry point pager needs a value for [" + CTX_FACTORY_RESOLVER + "] to be specified in GemEntryPointPager's init-param in web.xml.");
		loadContextFactoryResolverImplementation(resolverDetail);
	}
	
	/**
	 * Invokes a factory method to create a AcionEntryPointPage object which
	 * is then asked to render.
	 * @see org.javaongems.server.GemServe.Action#execute(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
	 */
	protected void renderPage(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		if (LOGGER.isDebugEnabled()) LOGGER.debug("attempting to render page");
		try {
			EntryPointPage gemPage = constructEntryPointPage(req);
			gemPage.initialise(req, resp, gwtModuleName, gwtEntryPointPage);
			gemPage.render();
		} catch (ExpressRedirector.ShortCircuit err) {
			// ignore this
		}
	}

	/**
	 * Factory method that is used to bridge the factory resolver and page context 
	 * factories together. 
	 * based on the request's state.
	 * @param req - the http request whose query string cause the construction 
	 * 			of different page types.
	 * @return an EntryPointPage object that is specific to the request
	 */
	protected EntryPointPage constructEntryPointPage(HttpServletRequest req) {
		ContextFactory cf = factoryResolver.getContextFactory(req, pageFactoryContexts);
		return cf.create(req);
	}
	
	
	/**
	 * Registers the context factory resolver that will be used to query the
	 * appropriate context factory to for each request.
	 * @param resolver - the context factory resoolver to 
	 * @throws ServletException
	 */
	protected void loadContextFactoryResolverImplementation(String resolver) 
			throws ServletException {
		try {
			Class clz = Class.forName(resolver);
			factoryResolver = (ContextFactoryResolver) clz.newInstance();
			factoryResolver.setServletConfig(servletConfig, pageFactoryContexts);
			LOGGER.info("registering context factory resolver [" + resolver + "]=" + factoryResolver);
		} catch (Exception err) {
			throw new ServletException( "unable to initialise Gems context factory resolver [" + resolver + "]");
		}
	}
	
	/**
	 * Registers the page context factories implementations for use with page construction later.
	 * @param props - a map factory implementations and their names 
	 * @throws ServletException
	 */
	protected void loadContextFactoriesImplementations(Properties props) 
			throws ServletException {
		for (Iterator iter = props.keySet().iterator(); iter.hasNext();) {
			String name = (String) iter.next();
			String val = props.getProperty(name);
			try {
				Class clz = Class.forName(val);
				ContextFactory impl = (ContextFactory) clz.newInstance();
				pageFactoryContexts.put(name, impl);
				LOGGER.info("registering page context factory [" + name + "]=" + impl);
			} catch (Exception err) {
				throw new ServletException("unable to initialise Gems page context factory [" + val + "]");
			}
		}
	}
	
	/**
	 * Specification to resolve context factories based on the request. It is 
	 * important to understand that contexts are user-defined and opaque to
	 * the GemEntryPointPager. Therefore, deployers must provide a resolver
	 * that is able to make sence of the context.
	 */
	static public interface ContextFactoryResolver {
		public void setServletConfig(ServletConfig cfg, Map<String, ContextFactory> contexts) throws ServletException;
		public ContextFactory getContextFactory(HttpServletRequest req, Map<String, ContextFactory> contexts);

	}
	
	/**
	 * Specification to construct an BaseEntryPointPage. This is used with 
	 * the page.context.factories configuration paramater.
	 */
	static public interface ContextFactory {
		public EntryPointPage create(HttpServletRequest req);
	}
	
	// HttpServlet overrides
	protected void service(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		threadsRequest.set(req);
		threadsPrincipal.set(PrincipalUtils.getPrincipal(req));
		try {
			super.service(req, resp);
		} finally {
			threadsRequest.set(null);
			threadsPrincipal.set(null);
		}
	}
	
	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
		throws ServletException, IOException {
		renderPage(req, resp);
	}
	
	protected void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		renderPage(req, resp);
	}

	// helpers
	static public HttpServletRequest getThreadLocalHttpServletRequest() {
		return threadsRequest.get();
	}
	
	static public Principal getThreadLocalPrincipal() {
		return threadsPrincipal.get();
	}

	/**
	 * Returns the stack trace of the throwable as a string
	 * @param t - the throwable object to determine its stack trace
	 * @return stringified stack trace
	 */
	static public String stackTraceAsString(Throwable t) {
		if (t == null) return "[root cause unavailable]";
		StringWriter sw = new StringWriter();
		PrintWriter pw = new PrintWriter(sw);
		t.printStackTrace(pw);
		return sw.toString();
	}
}
